﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;

namespace DateRangeSlider
{
  public class Coordinator : FrameworkElement
  {
    private readonly DispatcherTimer _timer;

    public double AbsoluteMin
    {
      get { return _absoluteMin; }
      set
      {
        _absoluteMin = value;
        if (Content != null) Content.AbsoluteMin = _absoluteMin;
      }
    }
    public double AbsoluteMax
    {
      get { return _absoluteMax; }
      set
      {
        _absoluteMax = value;
        if (Content != null) Content.AbsoluteMax = _absoluteMax;
      }
    }
    public double ViewPortMin
    {
      get { return _viewPortMin; }
      set
      {
        _viewPortMin = value;
        if (Content != null) Content.ViewPortMin = _viewPortMin;
      }
    }
    public double ViewPortMax
    {
      get { return _viewPortMax; }
      set
      {
        _viewPortMax = value;
        if (Content != null) Content.ViewPortMax = _viewPortMax;
      }
    }
    public double RangeMin
    {
      get { return _rangeMin; }
      set
      {
        _rangeMin = value;
        if (Content != null) Content.RangeMin = _rangeMin;
      }
    }
    public double RangeMax
    {
      get { return _rangeMax; }
      set
      {
        _rangeMax = value;
        if (Content != null) Content.RangeMax = _rangeMax;
      }
    }

    public DateRange Content
    {
      get { return _content; }
      set
      {
        _content = value;

        if (Content == null) return;
        Content.AbsoluteMin = _absoluteMin;
        Content.AbsoluteMax = _absoluteMax;
        Content.ViewPortMin = _viewPortMin;
        Content.ViewPortMax = _viewPortMax;
        Content.RangeMin = _rangeMin;
        Content.RangeMax = _rangeMax;
      }
    }
    public FrameworkElement Left { get; set; }
    public Thumb Center { get; set; }
    public FrameworkElement Right { get; set; }
    public Thumb MinThumb { get; set; }
    public Thumb MaxThumb { get; set; }

    private IEnumerable<FrameworkElement> InternalChildren
    {
      get
      {
        var visualChildrenCount = VisualChildrenCount;
        for (var i = 0; i < visualChildrenCount; i++)
          yield return (FrameworkElement)GetVisualChild(i);
      }
    }
    protected override Visual GetVisualChild(int index)
    {
      switch (index)
      {
        case 0: return Content;
        case 1: return Left;
        case 2: return Center;
        case 3: return Right;
        case 4: return MinThumb;
        case 5: return MaxThumb;
        default: throw new ArgumentOutOfRangeException("index");
      }
    }
    protected override int VisualChildrenCount { get { return 6; } }

    protected override Size MeasureOverride(Size availableSize)
    {
      foreach (var e in InternalChildren)
        if (e != null)
          e.Measure(availableSize);
      return availableSize;
    }
    protected override Size ArrangeOverride(Size arrangeSize)
    {
      var viewPortScreen = arrangeSize.Width - 0;
      if (viewPortScreen <= 0) return arrangeSize;

      _viewPortToScreen = new Tr(-ViewPortMin) * new Tr(0, viewPortScreen / ViewPort);

      var rangeMin = _viewPortToScreen.Transform(RangeMin);
      var rangeMax = _viewPortToScreen.Transform(RangeMax);

      if (arrangeSize.Height <= 0) return arrangeSize;

      MinThumb.Arrange(new Rect(new Point(
        rangeMin - MinThumb.DesiredSize.Width / 2,
        arrangeSize.Height / 2 - MinThumb.DesiredSize.Height / 2),
        MinThumb.DesiredSize));
      MaxThumb.Arrange(new Rect(new Point(
        rangeMax - MaxThumb.DesiredSize.Width / 2,
        arrangeSize.Height / 2 - MaxThumb.DesiredSize.Height / 2),
        MaxThumb.DesiredSize));

      if (Content != null && arrangeSize.Width > 0)
        Content.Arrange(new Rect(0, 0, arrangeSize.Width - 0, arrangeSize.Height));
      if (Left != null && rangeMin > 0)
        Left.Arrange(new Rect(0, 0, rangeMin - 0, arrangeSize.Height));
      if (Center != null && rangeMax > rangeMin)
        Center.Arrange(new Rect(rangeMin, 0, rangeMax - rangeMin, arrangeSize.Height));
      if (Right != null && arrangeSize.Width > rangeMax)
        Right.Arrange(new Rect(rangeMax, 0,arrangeSize.Width - rangeMax, arrangeSize.Height));

      return arrangeSize;
    }

    protected override IEnumerator LogicalChildren
    {
      get
      {
        yield return Content;
        yield return Left;
        yield return Center;
        yield return Right;
        yield return MinThumb;
        yield return MaxThumb;
      }
    }

    public Coordinator()
    {
      Initialized += OnInitialized;

      _timer = new DispatcherTimer(
        TimeSpan.FromMilliseconds(50),
        DispatcherPriority.Normal, Callback,
        Dispatcher.CurrentDispatcher) { IsEnabled = false };
    }

    protected override void OnMouseWheel(MouseWheelEventArgs e)
    {
      var range = Math.Max(Content.MinDelta, ViewPort * (e.Delta < 0 ? 1.2 : 0.8));
      var d = (ViewPort - range)/2;
      ViewPortMin = Math.Max(AbsoluteMin, ViewPortMin + d);
      ViewPortMax = Math.Min(AbsoluteMax, ViewPortMax - d);

      RangeMin = Math.Max(RangeMin, ViewPortMin);
      RangeMax = Math.Min(RangeMax, ViewPortMax);

      if (RangeMax - RangeMin < Content.MinDelta)
        RangeMin = Math.Max(RangeMin - (Content.MinDelta - (RangeMax - RangeMin)), ViewPortMin);

      if (RangeMax - RangeMin < Content.MinDelta)
        RangeMax = Math.Min(RangeMax + (Content.MinDelta - (RangeMax - RangeMin)), ViewPortMax);

      Invariant();
      InvalidateArrange();
    }

    private bool Invariant()
    {
      if (AbsoluteMin > ViewPortMin) return false;
      if (ViewPortMin > RangeMin) return false;
      if (RangeMin + Content.MinDelta > RangeMax) return false;
      if (RangeMax > ViewPortMax) return false;
      if (ViewPortMax > AbsoluteMax) return false;
      return true;
    }

    private void OnInitialized(object sender, EventArgs eventArgs)
    {
      for (var i = 0; i < VisualChildrenCount; i++)
        AddVisualChild(GetVisualChild(i));

      if (MinThumb == null || MaxThumb == null)
        return;

      Center.DragDelta += OnTrackDragDelta;
      Center.DragCompleted += (s, e) => _timer.Stop();
      MinThumb.DragDelta += OnRangeMinDragDelta;
      MaxThumb.DragDelta += OnRangeMaxDragDelta;
      Content.DragDelta += OnContentDragDelta;
    }

    private Tr _viewPortToScreen;
    private double _viewPortMin;
    private double _viewPortMax;
    private double _rangeMin;
    private double _rangeMax;
    private double _absoluteMin;
    private double _absoluteMax;
    private DateRange _content;
    private double _dx;

    private void OnTrackDragDelta(object sender, DragDeltaEventArgs e)
    {
      var dx = _viewPortToScreen.Invert()
        .TransformVector(e.HorizontalChange);

      RangeMin += dx;
      RangeMax += dx;

      if (RangeMax > ViewPortMax)
      {
        var corr = ViewPortMax - RangeMax;
        RangeMin += corr;
        RangeMax += corr;
      }

      if (RangeMin < ViewPortMin)
      {
        var corr = ViewPortMin - RangeMin;
        RangeMin += corr;
        RangeMax += corr;
      }

      _timer.IsEnabled = OnTheSide;

      InvalidateArrange();
    }
    private void OnRangeMinDragDelta(object sender, DragDeltaEventArgs e)
    {
      RangeMin = Math.Max(ViewPortMin, RangeMin +
        _viewPortToScreen.Invert().TransformVector(e.HorizontalChange));
      InvalidateArrange();
    }
    private void OnRangeMaxDragDelta(object sender, DragDeltaEventArgs e)
    {
      RangeMax = Math.Min(ViewPortMax, RangeMax +
        _viewPortToScreen.Invert().TransformVector(e.HorizontalChange));
      InvalidateArrange();
    }
    private void OnContentDragDelta(object sender, DragDeltaEventArgs e)
    {
      var dx = -_viewPortToScreen.Invert().TransformVector(e.HorizontalChange - _dx);
      _dx = e.HorizontalChange;

      RangeMin += dx;
      RangeMax += dx;
      ViewPortMin += dx;
      ViewPortMax += dx;
      if (ViewPortMax > AbsoluteMax)
      {
        var corr = AbsoluteMax - ViewPortMax;
        RangeMin += corr;
        RangeMax += corr;
        ViewPortMin += corr;
        ViewPortMax += corr;
      }

      if (ViewPortMin < AbsoluteMin)
      {
        var corr = AbsoluteMin - ViewPortMin;
        RangeMin += corr;
        RangeMax += corr;
        ViewPortMin += corr;
        ViewPortMax += corr;
      }

      InvalidateArrange();
    }

    private void Callback(object sender, EventArgs eventArgs)
    {
      var inc = Inc;
      if (OnTheLeft)
      {
        ViewPortMin -= inc;
        RangeMin -= inc;
        RangeMax -= inc;
        ViewPortMax -= inc;
      }
      else if (OnTheRight)
      {
        ViewPortMin += inc;
        RangeMin += inc;
        RangeMax += inc;
        ViewPortMax += inc;
      }
      if (!OnTheSide) _timer.Stop();
    }

    #region ' Calc '

    public const double Threshold = .1;
    public const double Steps = 4;

    public double Range { get { return RangeMax - RangeMin; } }
    public double ViewPort { get { return ViewPortMax - ViewPortMin; } }
    public double Ratio { get { return Range > ViewPort / 2 ? .5 : Math.Min((RangeMin - ViewPortMin) / ViewPort, (ViewPortMax - RangeMax) / ViewPort); } }
    public bool OnTheLeft { get { return Ratio < Threshold && (RangeMin - ViewPortMin) / ViewPort <= (ViewPortMax - RangeMax) / ViewPort && ViewPortMin > AbsoluteMin; } }
    public bool OnTheRight { get { return Ratio < Threshold && (RangeMin - ViewPortMin) / ViewPort > (ViewPortMax - RangeMax) / ViewPort && ViewPortMax < AbsoluteMax; } }
    public bool OnTheSide { get { return OnTheLeft || OnTheRight; } }
    /// <summary>(slower) 0..1 (faster)</summary>
    public double Velocity { get { return 2 * Threshold / (Threshold + Ratio) - 1; } }
    public double Inc
    {
      get
      {
        var inc = Math.Pow(((AbsoluteMax - AbsoluteMin) - ViewPort) / Steps, Velocity);
        return OnTheLeft 
          ? Math.Min(ViewPortMin - AbsoluteMin, inc) 
          : Math.Min(AbsoluteMax - ViewPortMax, inc);
      }
    }

    #endregion

  }
}